import Lexer from '../lexer'
import sign, {
  keyOf
} from '../ast/sign'

const
  FAIL = sign.fail

function lex (src, method) {
  let
    lines = [], notes = [], fact = [],
    lexer = new Lexer(src, '')

  lexer.collector = {
    newLine: function (col) {lines.push(col)},
    newNote: function (note) {notes.push(note)},
  }

  method = method || 'next'

  let x = lexer[method]()
  while (x && x.sign !== FAIL) {
    if (x.sign) fact.push(x)
    x = lexer[method]()
  }
  return [fact, lexer.error, lines, notes]
}

function keys (fact) {
  return fact.map(x => keyOf(x.sign))
}

function deepKeys (fact) {
  return fact.reduce((a, x) => {
    a.push(keyOf(x.sign))
    if (x.body && x.body.length)
      a.push(deepKeys(x.body))
    return a
  }, [])
}

function source (fact) {
  return fact.map(x => {
    return !x.hasOwnProperty('data') && 'S' || x.data
  })
}

function offset (fact) {
  return fact.map(x => {
    return !x.start && [] ||
    [x.start.offset, x.end.offset]
  })
}

function deepAll (fact) {
  return fact.reduce((a, x) => {
    let b = getAll(x)
    a.push(b)
    if (x.body && x.body.length)
      b.push(...deepAll(x.body))
    return a
  }, [])
}

function getAll (x) {
  return [
    keyOf(x.sign),
    x.start.offset, x.start.line, x.start.column,
    x.end.offset, x.end.line, x.end.column
  ]
}
function rowcol (fact) {
  return fact.map(x => {
    return !x.start && [] ||
    [
      x.start.line, x.start.column,
      x.end.line, x.end.column
    ]
  })
}

function compose (...x) {
  let src = ' ', goal = []
  for (var i = 0; i < x.length; i++) {
    let n =
      x[i + 1] === -1 && merge(x[i++]) ||
      typeof x[i + 1] === 'number' &&
      x[i++][x[i]] || x[i][0]

    src += n[0] + ' '
    goal = goal.concat(n.slice(1))
  }
  return [goal, ...lex(src, 'fragment')]
}

function merge (x) {
  let goal = [' ']
  for (var i = 0; i < x.length; i++) {
    goal[0] += x[i][0] + ' '
    goal.push(...x[i].slice(1))
  }
  return goal
}

function fragment (src, all) {
  return lex(src, 'fragment', all)
}

function notesText (notes) {
  return notes.reduce((array, n) => {
    if (n.body)
      n.body.forEach(n => array.push(n.data))
    else
      array.push(n.data)
    return array
  }, [])
}

const
  number = [
    ['0', 'i8'],
    ['00', 'u8'],
    ['1', 'i8'],
    ['2', 'i8'],
    ['3', 'i8'],
    ['4', 'i8'],
    ['5', 'i8'],
    ['6', 'i8'],
    ['7', 'i8'],
    ['8', 'i8'],
    ['9', 'i8'],
    ['127', 'i8'],
    ['128', 'i16'],
    ['32767', 'i16'],
    ['32768', 'i32'],
    ['2147483647', 'i32'],
    ['2147483648', 'i64'],
    ['0255', 'u8'],
    ['0256', 'u16'],
    ['065535', 'u16'],
    ['065536', 'u32'],
    ['04294967295', 'u32'],
    ['04294967296', 'u64'],

    ['0b0', 'u8'],
    ['0b1', 'u8'],
    ['0b10011001', 'u8'],
    ['0b100110010', 'u16'],
    ['0b1001100110011001', 'u16'],
    ['0b10011001100110000', 'u32'],
    ['0b10011001100110001001100110011000', 'u32'],
    ['0b100110011001100010011001100110000', 'u64'],

    ['0f01234567', 'f32'],
    ['0fabcdABCD', 'f32'],
    ['0f01234567abcdABCD', 'f64'],
    ['0ffFfFab102CdEF900', 'f64'],
    ['0x0a', 'u8'],
    ['0x1b', 'u8'],
    ['0x2c', 'u8'],
    ['0x3d', 'u8'],
    ['0x4e', 'u8'],
    ['0x5f', 'u8'],
    ['0x6A', 'u8'],
    ['0x7B', 'u8'],
    ['0x8C', 'u8'],
    ['0x9D', 'u8'],
    ['0xEF', 'u8'],
    ['0x0000', 'u8'],
    ['0xFFFF', 'u16'],
    ['0xFfFFFFfF', 'u32'],
    ['0x0FFfFFfFF', 'u32'],
    ['0xFfFFFFfF0', 'u64'],

    ['12342.09948284', 'float'],
    ['923e0123456789', 'float'],
    ['031E0123456789', 'float'],
    ['23e+0123456789', 'float'],
    ['45E-0123456789', 'float'],

    ['Infinity', 'Infinity'],
    ['NaN', 'NaN'],
  ],
  punc = [
    [':', ':'],
    // ['$', '$'],
    ['!', '!'],
    ['_', '_'],
    // ['#', '#'],
    // ['@', '@'],
    ['?', '?'],
  ],
  mp = [
    ['--', '--'],
    ['++', '++'],
  ],
  op = [ // keep order
    ['&', '&'],
    ['**', '**'],
    ['*', '*'],
    ['.', '.'],
    ['^', '^'],
    ['..', '..'],
    ['>>>', '>>>'],
    ['>>', '>>'],
    ['<<', '<<'],
    ['>', '>'],
    ['<', '<'],
    ['-', '-x'],
    ['%', '%'],
    ['+', '+x'],
    ['/', '/'],
    ['~', '~'],
    ['|', '|'],
  ],
  eq = [ // keep order
    ['=', '='],
    ['&=', '&='],
    ['*=', '*='],
    ['**=', '**='],
    ['^=', '^='],
    ['>>=', '>>='],
    ['>>>=', '>>>='],
    ['>=', '>='],
    ['<<=', '<<='],
    ['<=', '<='],
    ['-=', '-='],
    ['%=', '%='],
    ['+=', '+='],
    ['/=', '/='],
    ['~=', '~='],
    ['|=', '|='],
  ],
  opword = [
    [ 'and', 'and'],
    [ 'in', 'in'],
    [ 'is', 'is'],
    [ 'isnot', 'isnot'],
    [ 'not', 'not'],
    [ 'notin', 'notin'],
    [ 'or', 'or'],
    [ 'un', 'un'],
  ],
  ident = [
    ['breaki', 'ident'],
    ['catchi', 'ident'],
    ['consti', 'ident'],
    ['continuei', 'ident'],
    ['deferi', 'ident'],
    ['echoi', 'ident'],
    ['elifi', 'ident'],
    ['elsei', 'ident'],
    ['enumi', 'ident'],
    ['errori', 'ident'],
    ['fni', 'ident'],
    ['fori', 'ident'],
    ['ifi', 'ident'],
    ['iotai', 'ident'],
    ['ofi', 'ident'],
    ['outi', 'ident'],
    ['statici', 'ident'],
    ['superi', 'ident'],
    ['thisi', 'ident'],
    ['throwi', 'ident'],
    ['traiti', 'ident'],
    ['tryi', 'ident'],
    ['typei', 'ident'],
    ['usei', 'ident'],
    ['vari', 'ident'],
    ['yieldi', 'ident'],

    ['brea', 'ident'],
    ['catc', 'ident'],
    ['cons', 'ident'],
    ['continu', 'ident'],
    ['defe', 'ident'],
    ['ech', 'ident'],
    ['eli', 'ident'],
    ['els', 'ident'],
    ['enu', 'ident'],
    ['erro', 'ident'],
    ['fo', 'ident'],
    ['iot', 'ident'],
    ['ou', 'ident'],
    ['stati', 'ident'],
    ['supe', 'ident'],
    ['thi', 'ident'],
    ['thro', 'ident'],
    ['trai', 'ident'],
    ['tr', 'ident'],
    ['typ', 'ident'],
    ['us', 'ident'],
    ['va', 'ident'],
    ['yiel', 'ident'],

    ['b', 'ident'],
    ['c', 'ident'],
    ['d', 'ident'],
    ['e', 'ident'],
    ['f', 'ident'],
    ['i', 'ident'],
    ['o', 'ident'],
    ['s', 'ident'],
    ['t', 'ident'],
    ['u', 'ident'],
    ['v', 'ident'],
    ['y', 'ident'],

    ['""', 'ident'],

    ['"世界"', 'ident'],

    ['no-t', 'ident'],
    ['a0', 'ident'],
    ['Z9', 'ident'],
    ['a-y', 'ident'],
    ['A-9', 'ident'],
    ['a-Z9-09', 'ident'],
    ['A-9z-90', 'ident'],
    ['""', 'ident'],
    ['"`\'"', 'ident'],
    ['" "', 'ident'],
    ['"if z=1"', 'ident'],
    ['世界', 'ident'],
    ['世-09-界', 'ident'],
    ['ワード', 'ident'],
    ['мир', 'ident'],
    ['Svět', 'ident'],
    ['โลก', 'ident'],
    ['दुनिया', 'ident'],
    ['Κόσμος', 'ident'],
    ['Дэлхий', 'ident'],
    ['〇𒑮', 'ident'] // Nl U+3007 U+1246E
  ]

describe('token name', () => {
  it('should be work', () => {
    expect(keyOf(0)).to.be.equal('dividing-line')
    expect(keyOf(sign.break)).to.be.equal('break')
    expect(keyOf(sign['>='])).to.be.equal('>=')
  })
})

describe('simple tokens', () => {
  it('should be work', () => {
    let [goal, fact, error, lines] = compose(
      number, -1,
      punc, -1,
      mp, -1,
      op, -1,
      eq, -1,
      opword, -1,
      ident, -1
    )

    expect(error).to.be.equal('')
    expect(keys(fact)).to.deep.equal(goal)
    expect(lines).to.deep.equal([])
  })

  it('number location', () => {
    let src = 'const 0 1-1 2-2 0-3 0b0-1 0x0-5 6e-6-6 7e+7-7 8-8.8-8E-8 0f99A-Bcd-eF',
      goal = ['const',
        'i8', 'i8', 'i8',
        'u8', 'u8', 'u8',
        'float', 'float', 'float', 'f32'
      ],
      c = 0,
      [fact, error, lines] = lex(src)

    expect(error).to.be.equal('')
    expect(keys(fact)).to.deep.equal(goal)
    expect(lines).to.deep.equal([1])

    expect(offset(fact)).to.deep.equal(
      src.split(' ').map((s, i) => {
        i = c
        c = c + s.length + 1
        return [i, c - 1]
      })
    )
    expect(rowcol(fact)).to.deep.equal(
      src.split(' ').map((s, i) => {
        i = c
        c = c + s.length + 1
        return [1, i + 1, 1, c]
      }, c = 0)
    )
    expect(fact.slice(1).map(n => n.data)).to.deep.equal(
      [0, 11, 22, 3, 1, 5, 6, 7, 88, '99abcdef']
    )

    fact = fact.slice(7, 10).map(n => n.body[0]).concat(fact[9].body[1])
    expect(keys(fact)).to.deep.equal(['exponent', 'exponent', 'significand', 'exponent'])
    expect(offset(fact)).to.deep.equal([
      [src.indexOf('e-6'), src.indexOf(' 7')],
      [src.indexOf('e+7'), src.indexOf(' 8')],
      [src.indexOf('.8'), src.indexOf('E-8')],
      [src.indexOf('E-8'), src.indexOf(' 0f')],
    ])
    expect(fact.map(n => n.data)).to.deep.equal(
      [-66, 77, 88, -8]
    )
  })
})

describe('operators', () => {
  it('compact', () => {
    let goal = [], src = '';
    [
      op, mp, eq // keep order
    ].forEach(a => {
      a.forEach(a => {
        src += a[0]
        goal = goal.concat(a.slice(1))
      })
    })
    let [fact, error] = fragment(src)

    expect(keys(fact)).to.deep.equal(goal)
    expect(error).to.equal('')
  })
})

describe('top-declaration keywords', () => {
  it('use', () => {
    let src = 'use x "" "()"',
      goal = ['use', 'ident', 'ident', 'ident'],
      [fact, error, lines] = lex(src)

    expect(error).to.be.equal('')
    expect(keys(fact)).to.deep.equal(goal)
    expect(lines).to.deep.equal([1])

    expect(source(fact)).to.deep.equal([
      'S', 'x', '""', '"()"'
    ])
    expect(offset(fact)).to.deep.equal([
      [0, 3], [4, 5], [6, 8], [9, 13]
    ])
    expect(rowcol(fact)).to.deep.equal([
      [1, 1, 1, 4], [1, 5, 1, 6], [1, 7, 1, 9], [1, 10, 1, 14]
    ])
  })

  it('const', () => {
    let src = 'pub const\nconst id',
      goal = ['const', 'const', 'ident'],
      [fact, error, lines] = lex(src)

    expect(error).to.be.equal('')

    expect(keys(fact)).to.deep.equal(goal)
    expect(lines).to.deep.equal([1, 1])

    expect(source(fact)).to.deep.equal([
      'pub', 'S', 'id'
    ])
    expect(offset(fact)).to.deep.equal([
      [0, 9], [10, 15], [16, 18]
    ])
    expect(rowcol(fact)).to.deep.equal([
      [1, 1, 1, 10], [2, 1, 2, 6], [2, 7, 2, 9]
    ])
  })

  it('type', () => {
    let src = 'pub type\ntype x extends b traits  c',
      goal = [
        'type', 'ident', 'extends', 'ident', 'traits', 'ident'
      ],
      [fact, error] = lex(src)

    expect(error).to.be.equal('')
    expect(keys(fact)).to.deep.equal(goal)

    expect(source(fact)).to.deep.equal([
      'S', 'x', 'S', 'b', 'S', 'c'
    ])
    expect(offset(fact)).to.deep.equal([
      [9, 13], [14, 15], [16, 23],
      [24, 25], [26, 32], [34, 35]
    ])
    expect(rowcol(fact)).to.deep.equal([
      [2, 1, 2, 5], [2, 6, 2, 7], [2, 8, 2, 15],
      [2, 16, 2, 17], [2, 18, 2, 24], [2, 26, 2, 27],
    ])
  })

  it('trait', () => {
    let src = 'pub trait \ntrait x extends int',
      goal = ['trait', 'trait', 'ident', 'extends', 'ident'],
      [fact, error, lines] = lex(src)

    expect(error).to.be.equal('')
    expect(keys(fact)).to.deep.equal(goal)
    expect(lines).to.deep.equal([1, 1])

  })

  it('enum', () => {
    let src = 'pub enum \nenum x extends int',
      goal = ['enum', 'enum', 'ident', 'extends', 'ident'],
      [fact, error, lines] = lex(src)

    expect(error).to.be.equal('')
    expect(keys(fact)).to.deep.equal(goal)
    expect(lines).to.deep.equal([1, 1])
  })

  it('fn', () => {
    let src = 'extern fn\nfn x (int x,out this,yield)',
      goal = [
        'fn', 'ident', '()'
      ],
      params = [
        'ident', 'ident', ',', 'out', 'this', ',', 'yield'
      ],
      [fact, error, lines] = lex(src)

    expect(error).to.be.equal('')
    expect(keys(fact)).to.deep.equal(goal)
    expect(keys(fact[2].body)).to.deep.equal(params)
    expect(lines).to.deep.equal([0, 1])

    expect(source(fact)).to.deep.equal([
      'S', 'x', 'S'
    ])
    expect(source(fact[2].body)).to.deep.equal([
      'int', 'x', 'S', 'S', 'S', 'S', 'S'
    ])

    expect(offset(fact)).to.deep.equal([
      [10, 12], [13, 14], [15, 37]
    ])

    expect(offset(fact[2].body)).to.deep.equal([
      [16, 19], [20, 21], [21, 22],
      [22, 25], [26, 30], [30, 31], [31, 36]
    ])

  })

  it('comment', () => {
    let src = 'const @pegz\n  ;note\n  a',
      goal = ['const', '@', 'ident', 'ident'],
      [fact, error, lines, notes] = lex(src)

    expect(error).to.be.equal('')
    expect(keys(fact)).to.deep.equal(goal)
    expect(lines).to.deep.equal([1, 0, 3])

    expect(source(fact)).to.deep.equal([
      'S', 'S', 'pegz', 'a'
    ])

    expect(offset(fact)).to.deep.equal([
      [0, 5], [6, 7], [7, 11], [22, 23]
    ])

    expect(rowcol(fact)).to.deep.equal([
      [1, 1, 1, 6], [1, 7, 1, 8], [1, 8, 1, 12], [3, 3, 3, 4]
    ])

    expect(keys(notes)).to.deep.equal(['comment'])
    expect(source(notes)).to.deep.equal([';note'])

  })
})

describe('comments', () => {
  it('note', () => {
    let src =
    `note\n\n  note\nconst
      x = 1 ;note
        ; note\n`,
      goal = ['const', 'ident', '=', 'i8'],
      [fact, error, lines, notes] = lex(src)

    expect(error).to.be.equal('')
    expect(keys(fact)).to.deep.equal(goal)
    expect(keys(notes)).to.deep.equal([
      'note', 'note', 'comment', 'comment'
    ])
    expect(notesText(notes)).to.deep.equal([
      'note', '  note', ';note', '; note'
    ])
    expect(lines).to.deep.equal([0, 0, 0, 1, 7, 0])
  })

  it('notes', () => {
    let src =
    `note\nconst
      ---
      ---
  ---notes

  notes
  ---notes\n`,
      goal = ['const'],
      [fact, error, lines, notes] = lex(src)

    expect(error).to.be.equal('')
    expect(keys(fact)).to.deep.equal(goal)

    expect(notesText(notes)).to.deep.equal([
      'note', '---', '---',
      '---notes', '', 'notes', '---notes',
    ])

    expect(lines).to.deep.equal(
      [0, 1, 0, 0, 0, 0, 0, 0, 0]
    )
  })
})

describe('string', () => {
  it('escape', () => {
    let
      src = '\'abc\\f\\t\\r\\v\\n\\\\\\x0F\\xaf\\x{10ffFF}def\'',
      raw = 'abc\f\t\r\v\n\\\x0F\xaf\u{10ffFF}def',
      [fact, error] = fragment(src)

    expect(error).to.equal('')
    expect(keys(fact)).to.deep.equal(['string'])
    expect(source(fact)).to.deep.equal([raw])
  })

  it('multi-line', () => {
    let src =
      `const
        'abc
        \\tdef
        hijk'`,
      raw = 'abc\n\tdef\nhijk',
      [fact, error, lines] = lex(src)

    expect(error).to.equal('')
    expect(fact[1].sign).to.equal(sign.string)
    expect(fact[1].data).to.equal(raw)

    expect(lines).to.deep.equal(
      [1, 9, 9, 9]
    )

  })
})

describe('brackets', () => {
  it('should be ok', () => {
    let x, src = 'const\n  a{}[](){[(-1)]}',
      goal = [
        ['const',
          0, 1, 1,
          5, 1, 6
        ],
        ['ident',
          x = src.indexOf('a'), 2, 3,
          x + 1, 2, 4
        ],
        ['{}',
          x = src.indexOf('{}'), 2, 4,
          x + 2, 2, 6
        ],
        ['[]',
          x = src.indexOf('[]'), 2, 6,
          x + 2, 2, 8
        ],
        ['()',
          x = src.indexOf('()'), 2, 8,
          x + 2, 2, 10
        ],
        ['{}',
          x = src.indexOf('{['), 2, 10,
          x + 8, 2, 18,
          ['[]',
            x = src.indexOf('[('), 2, 11,
            x + 6, 2, 17,
            ['()',
              x = src.indexOf('(-'), 2, 12,
              x + 4, 2, 16,
              ['-x',
                x = src.indexOf('-1'), 2, 13,
                x + 1, 2, 14,
              ],
              ['i8',
                x = src.indexOf('1'), 2, 14,
                x + 1, 2, 15,
              ],
            ]
          ]
        ]
      ],
      [fact, error, lines] = lex(src)

    expect(error).to.be.equal('')
    expect(lines).to.deep.equal(
      [1, 3]
    )
    expect(deepAll(fact)).to.deep.equal(goal)
  })
})

describe('template', () => {
  it('single line', () => {
    let
      x = 0,
      src = 'const\n  `a{{b+c}}{{d}}e` `{{b+c}}`',
      goal = ['const', 'template', 'template'],
      [fact, error] = lex(src)

    expect(error).to.be.equal('')
    expect(keys(fact)).to.deep.equal(goal)

    expect(offset(fact)).to.deep.equal([
      [0, 5], [8, 24], [25, 34]
    ])

    expect(rowcol(fact)).to.deep.equal([
      [1, 1, 1, 6], [2, 3, 2, 19], [2, 20, 2, 29]
    ])

    expect(keys(fact[1].body)).to.deep.equal([
      'text', 'embed', 'embed', 'text'
    ])

    expect(offset(fact[1].body)).to.deep.equal([
      [x = src.indexOf('a'), x + 1],
      [x + 1, x = src.indexOf('{{d')],
      [x, x = src.indexOf('e')],
      [x, x + 1]
    ])

    expect(keys(fact[1].body[1].body)).to.deep.equal([
      'ident', '+', 'ident'
    ])

    expect(keys(fact[1].body[2].body)).to.deep.equal([
      'ident'
    ])
    expect(keys(fact[2].body)).to.deep.equal([
      'embed'
    ])
    expect(keys(fact[2].body[0].body)).to.deep.equal([
      'ident', '+', 'ident'
    ])

    expect(source(fact[1].body)).to.deep.equal([
      'a', 'S', 'S', 'e'
    ])

    expect(source(fact[1].body[1].body)).to.deep.equal([
      'b', 'S', 'c'
    ])
    expect(source(fact[1].body[2].body)).to.deep.equal([
      'd'
    ])
    expect(source(fact[2].body[0].body)).to.deep.equal([
      'b', 'S', 'c'
    ])
  })

  it('multi-line', () => {
    let src =
      `const
      \`a{{
        b+c
        }}{{d}}e\`
      \`{{b+c}}\``,
      goal = ['const', 'template', 'template'],
      [fact, error, lines] = lex(src)

    expect(error).to.be.equal('')
    expect(keys(fact)).to.deep.equal(goal)

    expect(lines).to.deep.equal([
      1, 7, 7, 7, 7
    ])

    expect(keys(fact[1].body)).to.deep.equal([
      'text', 'embed', 'embed', 'text'
    ])

    expect(offset(fact[1].body)).to.deep.equal([
      [13, 14], [14, 39], [39, 44], [44, 45]
    ])

    expect(rowcol(fact[1].body)).to.deep.equal([
      [2, 8, 2, 9], [2, 9, 4, 11], [4, 11, 4, 16], [4, 16, 4, 17]
    ])

    expect(keys(fact[1].body[1].body)).to.deep.equal([
      'ident', '+', 'ident'
    ])

    expect(keys(fact[1].body[2].body)).to.deep.equal([
      'ident'
    ])
    expect(keys(fact[2].body)).to.deep.equal([
      'embed'
    ])
    expect(keys(fact[2].body[0].body)).to.deep.equal([
      'ident', '+', 'ident'
    ])

    expect(source(fact[1].body)).to.deep.equal([
      'a', 'S', 'S', 'e'
    ])

    expect(source(fact[1].body[1].body)).to.deep.equal([
      'b', 'S', 'c'
    ])
    expect(source(fact[1].body[2].body)).to.deep.equal([
      'd'
    ])
    expect(source(fact[2].body[0].body)).to.deep.equal([
      'b', 'S', 'c'
    ])

  })
})
